package com.netx.ebs;
import javax.servlet.ServletConfig;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.nio.charset.Charset;
import com.netx.generics.basic.Logger;
import com.netx.generics.basic.ErrorList;
import com.netx.generics.basic.Context;
import com.netx.generics.basic.PropertyDefinition;
import com.netx.generics.basic.RootContext;
import com.netx.generics.basic.ContextRules;
import com.netx.generics.basic.RulesException;
import com.netx.generics.translation.Results;
import com.netx.generics.translation.MessageFormatter;
import com.netx.generics.translation.BasicMessageFormatter;
import com.netx.generics.io.Directory;
import com.netx.generics.io.File;
import com.netx.generics.io.FileSystem;
import com.netx.generics.io.Location;
import com.netx.generics.time.TimeValue;
import com.netx.generics.time.TimeValue.MEASURE;
import com.netx.generics.util.Strings;
import com.netx.generics.util.Tools;
import com.netx.data.Connection;
import com.netx.data.ConnectionListener;
import com.netx.data.Database;
import com.netx.data.DatabaseException;


public abstract class InitializerServlet extends EbsServlet {

	protected InitializerServlet() {
		super(new SecurityChecker[0]);
	}

	public void ebsInit(ServletConfig config) throws Throwable {
		super.ebsInit(config);
		
		final RootContext rootCtx = Context.getRootContext();
		final EbsContext ebsCtx = getEbsContext();
		ebsCtx.setProperty(Constants.EBSCTX_INIT_CALLED, true);
		
		// Load init parameters:
		final String paramConfigDir = getInitParameter("config-dir");
		final String paramDatabaseConfFile = getInitParameter("database-conf-file");
		final String paramContextConfFiles = getInitParameter("context-conf-files");
		if(Strings.isEmpty(paramConfigDir)) {
			addInitError("required parameter 'config-dir' not found");
			return;
		}
		if(Strings.isEmpty(paramDatabaseConfFile)) {
			addInitError("required parameter 'database-conf-file' not found");
			return;
		}
		final String[] contextFiles;
		if(Strings.isEmpty(paramContextConfFiles)) {
			addInitError("required parameter 'context-conf-files' not found");
			return;
		}
		else {
			contextFiles = paramContextConfFiles.split("[;]");
		}

		// Load config dir:
		final Directory configDir;
		try {
			configDir = new FileSystem(new Location(paramConfigDir));
		}
		catch(FileNotFoundException fnfe) {
			addInitError("directory '"+paramConfigDir+"' not found");
			return;
		}
		catch(IOException io) {
			// Can only be AcessDenied or DeviceFailure:
			addInitError(io);
			return;
		}
		
		// Load config files:
		InputStream databaseIn = null;
		InputStream[] contextIn = new InputStream[contextFiles.length];
		boolean fileErrors = false;
		try {
			final File databaseXml = configDir.getFile(paramDatabaseConfFile);
			if(databaseXml == null) {
				addInitError("file '"+paramConfigDir+"/"+paramDatabaseConfFile+"' not found");
				fileErrors = true;
			}
			else {
				databaseIn = databaseXml.getInputStream();
			}
		}
		catch(IOException io) {
			// Can only be AcessDenied or DeviceFailure:
			addInitError(io);
			fileErrors = true;
		}
		for(int i=0; i<contextFiles.length; i++) {
			try {
				File contextXml = configDir.getFile(contextFiles[i]);
				if(contextXml == null) {
					addInitError("file '"+paramConfigDir+"/"+contextFiles[i]+"' not found");
					fileErrors = true;
				}
				else {
					contextIn[i] = contextXml.getInputStream();
				}
			}
			catch(IOException io) {
				// Can only be AcessDenied or DeviceFailure:
				addInitError(io);
				fileErrors = true;
			}
		}
		if(fileErrors) {
			return;
		}
		
		final MessageFormatter mf = new BasicMessageFormatter(new String[] {
			"scan error", "parse error", "semantic error", "rule error"
		});

		// Make sure Database is loaded and put inside the context first,
		// so that it exists when loading RootContext:
		Results dbr = Database.init(databaseIn);
		// If errors: show errors and warnings
		// If only warnings: log warnings
		if(dbr.hasErrors()) {
			addInitError(dbr.getMessages("errors loading database.xml", mf));
		}
		else if(dbr.hasWarnings()) {
			Tools.log(rootCtx.getLogger(), dbr.getMessages(null, "warnings loading database.xml", mf));
		}
		Database database = (Database)dbr.getContents();
		ebsCtx.setProperty("database", database);
		if(database != null) {
			database.addConnectionListener(new EbsConnectionListener(ebsCtx.internalGetSessionManager()));
		}
		
		// Load context config files:
		boolean foundCtxErrors = false;
		for(int i=0; i<contextIn.length; i++) {
			Results r = Context.loadFrom(contextIn[i]);
			if(r.hasErrors()) {
				foundCtxErrors = true;
				addInitError(r.getMessages("errors loading '"+contextFiles[i]+"'", mf));
			}
			else if(r.hasWarnings()) {
				Tools.log(rootCtx.getLogger(), r.getMessages(null, "warnings loading '"+contextFiles[i]+"'", mf));
			}
		}
		if(!foundCtxErrors) {
			// Define rules for config.xml:
			ContextRules rules = new ContextRules(false);
			rules.add(new PropertyDefinition("authenticator-name", String.class, false, false, PropertyDefinition.NO_DEFAULT_VALUE));
			rules.add(new PropertyDefinition("error-handler-name", String.class, false, false, PropertyDefinition.NO_DEFAULT_VALUE));
			rules.add(new PropertyDefinition("send-error-codes", boolean.class, false, false, true));
			rules.add(new PropertyDefinition("keep-sessions-on-reload", boolean.class, false, false, true));
			rules.add(new PropertyDefinition("session-mode", String.class, false, false, "normal", new String[] {"normal", "fixed-ip"}, PropertyDefinition.getIgnoreCaseComparator()));
			rules.add(new PropertyDefinition("allow-multiple-sessions", boolean.class, false, false, false));
			rules.add(new PropertyDefinition("session-check-daemon-period", TimeValue.class, false, false, PropertyDefinition.NO_DEFAULT_VALUE));
			rules.add(new PropertyDefinition("notification-check-daemon-period", TimeValue.class, false, false, PropertyDefinition.NO_DEFAULT_VALUE));
			rules.add(new PropertyDefinition("notification-keep-time", TimeValue.class, false, false, PropertyDefinition.NO_DEFAULT_VALUE));
			rules.add(new PropertyDefinition("charset", String.class, false, false, "UTF-8"));
			rules.add(new PropertyDefinition("save-errors-in-database", boolean.class, false, false, true));
			rules.add(new PropertyDefinition("show-stack-trace", boolean.class, false, false, true));
			// Rules for other properties:
			rules.add(new PropertyDefinition("database", Database.class, true, false, PropertyDefinition.NO_DEFAULT_VALUE));
			rules.add(new PropertyDefinition("application-name", String.class, true, false, PropertyDefinition.NO_DEFAULT_VALUE));
			rules.add(new PropertyDefinition("application-root", FileSystem.class, true, false, PropertyDefinition.NO_DEFAULT_VALUE));
			try {
				ebsCtx.applyRules(rules);
			}
			catch(RulesException re) {
				ErrorList el = re.getContents();
				if(el.getErrors() != null) {
					addInitError(el);
				}
				else {
					Tools.log(rootCtx.getLogger(), el);
				}
			}
		}
		else {
			// Unrecoverable:
			return;
		}

		// Check the 'charset' property:
		String charset = ebsCtx.getProperty("charset").stringValue();
		if(!Charset.isSupported(charset)) {
			addInitError("unsupported charset: "+charset);
		}
		// Check database entities:
		if(database != null) {
			try {
				Connection c = database.getConnection();
				// Check whether all required entities are present:
				String[] entities = new String[] {"users", "roles", "permissions", "permission_groups", "errors", "transgressions"};
				for(int i=0; i<entities.length; i++) {
					if(c.getEntity(entities[i]) == null) {
						addInitError(entities[i]+": entity not found");
					}
				}
				c.close();
			}
			catch(DatabaseException de) {
				addInitError(de);
			}
		}
		
		// Start notification-checker daemon:
		DaemonNotificationChecker notificationDaemon = new DaemonNotificationChecker(ebsCtx);
		TimeValue ndTime = ebsCtx.getNotificationCheckDaemonPeriod();
		rootCtx.getTimer().scheduleAtFixedRate(notificationDaemon, 0, ndTime.getAs(MEASURE.MILLISECONDS));
		// Start session-checker daemon:
		DaemonSessionChecker sessionDaemon = new DaemonSessionChecker(ebsCtx);
		TimeValue sdTime = ebsCtx.getSessionCheckDaemonPeriod();
		// Calculate delay:
		long delay = ndTime.getAs(MEASURE.MILLISECONDS) / 2;
		rootCtx.getTimer().scheduleAtFixedRate(sessionDaemon, delay, sdTime.getAs(MEASURE.MILLISECONDS));

		if(applicationInitialized()) {
			// Enclose user-defined Logger in an EbsLogger:
			if(ebsCtx.getSaveErrorsInDatabase()) {
				Logger l = rootCtx.getLogger();
				rootCtx.setProperty("logger", new EbsLogger(l, database));
			}
		}
	}
	
	public void ebsDestroy() throws Throwable {
		getEbsContext().getDatabase().close();
		super.ebsDestroy();
	}
	
	protected final void doGet(EbsRequest request, EbsResponse response) throws IOException {
		response.sendError(EbsResponse.SC_FORBIDDEN);
	}

	protected final void doPost(EbsRequest request, EbsResponse response) throws IOException {
		response.sendError(HttpServletResponse.SC_FORBIDDEN);
	}

	private class EbsConnectionListener implements ConnectionListener {
		private final SessionManager _sm;

		public EbsConnectionListener(SessionManager sm) {
			_sm = sm;
		}

		public void connectionRetrieved(Connection c) {
			Users u = (Users)c.getEntity("users");
			if(!u.isInited()) {
				u.init(_sm);
				((EbsEntity)c.getEntity("roles")).init(_sm);
				((EbsEntity)c.getEntity("permissions")).init(_sm);
				((EbsEntity)c.getEntity("errors")).init(_sm);
				((EbsEntity)c.getEntity("transgressions")).init(_sm);
			}
		}

		public void connectionClosed(Connection c) {
			// noop;
		}
	}
}
